#!/bin/bash
# 导入标志,确保只会被导入一次
common_functions_included=true
# 检查$1指定的端口服务是否上线,是返回0,否则返回1
# $1 服务端口号
function port_online(){
    netstat -nlp 2>/dev/null | grep ":$1" >/dev/null
}

# 等待服务端口上线,超时报错
# $1 服务端口号
# $2 超时时间(秒) 为0或空一直等
# $3 超时错误信息
# 返回 0正常,-1超时
function wait_port(){
    local timeout=$2
    [ -z "$timeout" ] && timeout=0
    [ "$timeout" -ge 0 ] || exit_on_error "INVALID timeout argument"
    local c=$timeout
    while ! port_online $1 ; 
    do 
        sleep 1;
        if [ $timeout -gt 0 ]  
        then
            c=$(expr $c - 1)
            if [ $c -eq 0 ] 
            then
                [ -z "$3" ]  || exit_on_error "错误:$3没有正常启动(ERROR:$3 timeout)"
                return 255
            fi
        fi
    done
}

# 通过检查端口，检查指定的服务是否上线
# $1 service name
# $2 default port of service
# $3 port 1
# $4 port 2
# 返回值：服务在线返回 0,否则返回 1
# 变量 $1_online 保存状态值：服务在线返回 0,否则返回 1
# 变量 $1_online_port 保存在线的服务端口，服务不在线则为空
function service_online(){
    eval $1_online=1
    eval $1_online_port=
    [ -n "$4" ]                      && port_online $4 && eval declare -g $1_online=0 && eval $1_online_port=$4 && return 0
    [ -n "$3" ] && [ "$3"  != "$4" ] && port_online $3 && eval declare -g $1_online=0 && eval $1_online_port=$3 && return 0
    [ -n "$2" ] && [ "$2"  != "$3" ] && port_online $2 && eval declare -g $1_online=0 && eval $1_online_port=$2 && return 0
    return 1
}
# 修改$1指定的文件名中名为$2的值为$3
# $1 key=value格式保存变量值的文件名
# $2 变量名key
# $3 变量的值value
# $4 sudo prefix
# 修改成功返回0, 失败退出脚本,$1 文件不存在则创建文件并添加key=value
function set_value(){
    if [ ! -f "$1" ]
    then 
        # 添加key=value对到文件
        [ ! -d "$(dirname $1)" ] && mkdir -p "$(dirname $1)"
        $4 echo -e "\n$2=$3" >> $1 || exit_on_error
        return 0
    fi
    if $4 grep -q -E "^\s*$2\s*=.*$" $1
    then
        local r="$3"
        r=${r//&/\\&}
        r=${r//!/\\!}
        r=${r//\//\\/}
        # 如果存在key=value定义，则修改定义,否则向文件添加定义
        $4 sed -i -r "s/(^\s*$2\s*=).*$/\\1$r/g" $1 || exit_on_error
        return 0
    fi
    if $4 grep -q -E "^(#+\s*)+$2\s*=.*$" $1
    then
        local r="$3"
        r=${r//&/\\&}
        r=${r//!/\\!}
        # 如果存在#开头的key=value(无效)定义,添加key=value对到找到行之后
        $4 sed -i -r "/^(#+\s*)+$2\s*=.*$/a\\$2=$r" "$1" || exit_on_error
        return 0
    fi
    # 添加key=value对到文件
    $4 bash -c "echo ;echo $2=$3 >> $1" || exit_on_error
    return 0  
}

# 返回 $1指定的文件名中名为$2的值
# $1 key=value格式保存变量值的文件名
# $2 变量名key
# 修改成功返回0, 返回 1
function get_value(){
    if [ ! -f "$1" ]
    then 
        return 1
    fi
    get_value_str "$(cat "$1")" "$2"
    return 0
}
# 返回 $1字符串中名为$2的值
# $1 key=value格式保存变量值的多行字符串
# $2 变量名key
# 修改成功返回0, 返回 1
function get_value_str(){
    # 搜索$2指定的关键字，有多行时只取最后一行
    local l=$(echo "$1" | grep  "^\s*$2\s*=" |sed '$!d')
    [ -z "$l" ] && return 1
    echo ${l#*=}
    return 0
}


# 修改配置文件( status )中名为$1的值为$2
# $1 变量名key
# $2 变量的值value
# 修改成功返回0, 失败退出脚本
function set_status_value(){
    set_value status "$1" "$2"
    #echo $1=$2
}
# 更新配置文件( status )中名为$1的值
# $1 变量名key
# 修改成功返回0, 失败退出脚本
function update_status_value(){
    set_value status "$1" "${!1}"
    #echo $1=${!1}
}

# 修改$1指定的文件名中名为$2的值为$3
# $1 key value格式保存变量值的文件名
# $2 变量名key
# $3 变量的值value
# $4 sudo prefix
# 修改成功返回0, 失败退出脚本,$1 文件不存在则创建文件并添加key=value
function set_config(){
    if [ ! -f "$1" ]
    then 
        # 添加key value对到文件
        [ ! -d "$(dirname $1)" ] && $4 mkdir -p "$(dirname $1)"
        $4 echo -e "\n$2 $3" >> $1 || exit_on_error
        return 0
    fi
        
    if $4 grep -q -E "^\s*$2\s+.*$" $1
    then
        local r="$3"
        r=${r//&/\\&}
        r=${r//!/\\!}
        # 如果存在key value定义，则修改定义,否则向文件添加定义
        $4 sed -i -r "s!(^\s*$2\s+).*!\\1$r!g" "$1" || exit_on_error
        return 0
    fi
    
    if $4 grep -q -E "^(#+\s*)+$2\s+.*$" $1
    then
        local r="$3"
        r=${r//&/\\&}
        r=${r//!/\\!}
        # 如果存在#开头的key value(无效)定义,添加key value对到找到行之后
        $4 sed -i -r "/^(#+\s*)+$2\s+.*/a\\$2 $r" "$1" || exit_on_error
        return 0
    fi

    $4 bash -c "echo ;echo $2 $3 >> $1" || exit_on_error
    return 0
}

# 显示错误信息的对话框
# $1 错误信息
# return 0 always
function error_message(){
    dialog --title "ERROR" --msgbox "$1" 0 0
}

# 显示错误信息的对话框
# $1 错误信息文件
# return 0 always
function error_log_message(){
    dialog --title "ERROR" --msgbox "$(cat $1)" 0 0
}
# 显示错误信息的对话框
# $1 错误信息
# return 0 always
function exit_on_error_message(){
    local c="$(call_stack)"
    # 从调用堆栈中删除当前函数名(即 exit_on_error_message )
    c="${c/${FUNCNAME[0]}/}"
    error_message "$1\n ${c/<-/}" 
    exit 255
}
# 显示错误信息的对话框
# $1 错误信息文件
# return 0 always
function exit_on_error_log(){
    local c="$(call_stack)"
    # 从调用堆栈中删除当前函数名(即 exit_on_error_log )
    c="${c/${FUNCNAME[0]}/}"
    dialog --title "ERROR" --msgbox "$(cat $1)\n ${c/<-/}" 0 0
    exit 255
}
# 输出函数调用堆栈
function call_stack(){
    local funs="${FUNCNAME[*]}"
    # 第一个空格替换为‘(调用堆栈):’
    funs="${funs/ /(调用堆栈):}"
    # 后续所有空格替换为‘<-’
    echo "${funs// / <- }"
}
# 上一条命令执行出错则中止脚本执行,并输出调用堆栈信息
function exit_on_error(){
    [  $? -eq 0 ] && return 
    echo "exit for error:$1 " 
    local c="$(call_stack)"
    # 从调用堆栈中删除当前函数名(即exit_on_error)
    c="${c/${FUNCNAME[0]}/}"
    echo "${c/<-/}"
    exit 255
}

# 显示信息的对话框
# $1 要显示的信息
# $2 对话框标题,为空不显示
# $3 背景标题,为空不显示
# return 0 always
function message_box(){
    local backtitle=
    [ -n "$3" ] && backtitle="--backtitle $3"
    local title=
    [ -n "$2" ] && title="--title $2"
    dialog $backtitle $title --msgbox "$1" 0 0
}

# 向栈中添加一个元素
# $1 栈变量名
# $2 添加到栈中的元素
function stack_push(){
    eval "$1=(\${$1[@]} "$2")"
}
# 从栈中弹出一个元素
# $1 栈变量名
# stack_pop_return中返回弹出的元素,如果栈为空则返回空
function stack_pop(){
    stack_pop_return=
    eval "local size=\${#$1[@]}"
    eval "[ $size -gt 0 ] && \
        stack_pop_return=\${$1[$size-1]} && \
        $1=(\${$1[@]:0:$(($size-1))})"
}

# 向帧队列中添加一个元素
# $1 添加到队列中的元素
function frame_queue_push(){
    stack_push frame_queue "$1"
}
# 从帧队列中弹出一个最后元素
# frame_queue_pop_return 中返回弹出的元素,如果队列为空则返回空
function frame_queue_pop(){
    stack_pop frame_queue
    frame_queue_pop_return="$stack_pop_return"
}

function frame_flow(){
    local frame_name=$1
    while [ -n "$frame_name" ]
    do
        # 将变量名定义的 map 转为定义字符串
        local dstr="$(declare -p $frame_name)"
        eval declare -A local frame=${dstr#*=}
        [ -z "${frame[fun]}" ] && break
        frame_queue_push $frame_name
        echo push: ${frame_queue[@]} >>frame_flow.log
        ${frame[fun]}        
        context_last_code=$?
        echo $frame_name $context_last_code >>frame_flow.log
        case $context_last_code in
            0|1|4) frame_name=${frame[$context_last_code]} ;;
            3) 
                frame_queue_pop
                frame_queue_pop
                echo pop: ${frame_queue[@]} >>frame_flow.log
                frame_name=$frame_queue_pop_return 
                ;;
            *) echo "无效返回值 $context_last_code" && exit 255 ;;
        esac
    done
    return $context_last_code
}

# 验证 sudo 用户密码是否正确
# $1 sudo 用户密码
# 密码有效返回 0,否则返回 1
function validate_sudo_pwd(){
    sudo -k
    echo "$1" | sudo -lS &>/dev/null
}
function check_sudo_user(){
    sudo -v &>/dev/null || exit_on_error_message "当前用户($(whoami))不是拥有管理权限(sudo)的用户"
}
function check_sudo_pwd(){
    validate_sudo_pwd "$sudo_pwd" || exit_on_error_message "[$sudo_pwd]不是当前用户($(whoami))有效密码"
}

# 从.分隔的property名转为_分隔的变量名并获取变量值
# $1 .分隔的property名字符串,such as server.port
function value_of_prop(){ 
    local n=${1//./_}
    echo ${!n}
}
# 返回 $1 指定的文件夹中所有文件名(含路径)
# $1 文件夹(绝对或相对路径)
# 返回的文件名包含路径 $1 开始的路径前缀
function get_files(){
    for element in `ls $1`
    do  
        local dir_or_file=$1"/"$element
        if [ -d $dir_or_file ]
        then 
            get_files $dir_or_file
        else
            echo $dir_or_file
        fi  
    done
}

# (iptables)检查防火墙是否开启了指定端口,开启返回0，否则返回1,参数错误返回 -1
# $1 端口号
# $2 协议类型
function firewall_port_opened_iptables(){
    # 检查端口是否已经添加,已经添加则返回 
    sudo iptables -C INPUT -p $2 --dport $1 -j ACCEPT 2>/dev/null && return 0
    return 1
}
# (firewall)检查防火墙是否开启了指定端口,开启返回0，否则返回1,参数错误返回 -1
# $1 端口号
# $2 协议类型
function firewall_port_opened_firewall(){
    # 检查端口是否已经添加,已经添加则返回
    sudo firewall-cmd --list-ports --zone=public | grep "$1/$2" >/dev/null && return 0
    return 1
}
# (ufw)检查防火墙是否开启了指定端口,开启返回0，否则返回1,参数错误返回 -1
# $1 端口号
# $2 协议类型
function firewall_port_opened_ufw(){
    # 检查端口是否已经添加,已经添加则返回 
    sudo ufw status | grep -E "$1/$2\s+ALLOW\s+Anywhere" >/dev/null && return 0
    return 1
}
# 检查防火墙是否开启了指定端口,开启返回0，否则返回1,参数错误返回 -1
# $1 端口号
# $2 协议类型，默认 tcp
function firewall_port_opened(){
    [[ "$1" =~ ^[0-9]+$  ]] || exit_on_error  "ERROR:invaid argument,\$1(port number) is not number "
    local prot=tcp
    [ -n "$2" ] && prot=$2
    if systemctl is-active firewalld >/dev/null 
    then 
        firewall_port_opened_firewall "$1" "$prot"
        return
    fi
    if systemctl is-active ufw >/dev/null 
    then 
        firewall_port_opened_ufw
        return
    fi
    if [ -e /etc/init.d/iptables ]
    then
        firewall_port_opened_iptables
    fi 
    return 1
}
# 开启防火墙指定端口(iptables)
# $1 端口号
# $2 协议类型
function firewall_open_port_iptables(){
    # 检查端口是否已经添加,已经添加则返回 
    firewall_port_opened_iptables $@ && return 0
    echo "开启防火墙端口(open firewall port $1/$2)--iptables"
    # 添加端口规则
    sudo iptables -A INPUT -p $2 --dport $1 -j ACCEPT || exit_on_error
}
# 开启防火墙指定端口(firewall)
# $1 端口号
# $2 协议类型
function firewall_open_port_firewall(){
    # 检查端口是否已经添加,已经添加则返回 
    firewall_port_opened_firewall $@ && return 0
    echo "开启防火墙端口(open firewall port $1/$2)--firewalld"
    # 添加端口规则
    sudo firewall-cmd --zone=public --add-port=$1/$2 --permanent || exit_on_error
}
# 开启防火墙指定端口(ufw)
# $1 端口号
# $2 协议类型
function firewall_open_port_ufw(){
    # 检查端口是否已经添加,已经添加则返回 
    firewall_port_opened_ufw $@ && return 0
    echo "开启防火墙端口(open firewall port $1/$2)--ufw"
    # 添加端口规则
    sudo ufw allow $1/$prot || exit_on_error
}
# 开启防火墙指定端口(iptables)
# $1 端口号
# $2 协议类型，默认 tcp
function firewall_open_port(){
    [[ "$1" =~ ^[0-9]+$  ]]  ||  exit_on_error  "ERROR:invaid argument,\$1(port number) is not number " 
    local prot=tcp
    [ -n "$2" ] && prot=$2
    sudo systemctl is-active firewalld >/dev/null \
        && firewall_open_port_firewall "$1" "$prot" && return 0
    sudo  systemctl is-active ufw >/dev/null \
        && firewall_open_port_ufw "$1" "$prot" && return 0
    [ -e /etc/init.d/iptables ] \
        && firewall_open_port_iptables "$1" "$prot" 
}


# 关闭防火墙指定端口(iptables)
# $1 端口号
# $2 协议类型
function firewall_close_port_iptables(){
    # 检查端口是否已经添加,端口未添加则返回 
    sudo iptables -C INPUT -p $2 --dport $1 -j ACCEPT 2>/dev/null || return 0
    echo "关闭防火墙端口(close firewall port $1/$2)--iptables"
    # 删除端口规则
    sudo iptables -D INPUT -p $2 --dport $1 -j ACCEPT || exit_on_error
}
# 关闭防火墙指定端口(firewall)
# $1 端口号
# $2 协议类型
function firewall_close_port_firewall(){
    # 检查端口是否已经添加,已经添加则返回 
    sudo firewall-cmd --list-ports --zone=public | grep "$1/$2" >/dev/null  || return 0
    echo "关闭防火墙端口(close firewall port $1/$2)--firewalld"
    # 删除端口规则
    sudo firewall-cmd --zone=public --remove-port=$1/$2 --permanent || exit_on_error
}
# 关闭防火墙指定端口(ufw)
# $1 端口号
# $2 协议类型
function firewall_close_port_ufw(){
    # 检查端口是否已经添加,已经添加则返回 
    sudo ufw status | grep -E "$1/$2\s+ALLOW\s+Anywhere" >/dev/null || return 0
    echo "关闭防火墙端口(close firewall port $1/$2)--ufw"
    # 删除端口规则
    sudo ufw delete allow $1/$2 || exit_on_error
}
# 关闭防火墙指定端口
# $1 端口号
# $2 协议类型，默认 tcp
function firewall_close_port(){
    [[ "$1" =~ ^[0-9]+$  ]]  ||  exit_on_error  "ERROR:invaid argument,\$1(port number) is not number " 
    local prot=tcp
    [ -n "$2" ] && prot=$2
    systemctl is-active firewalld >/dev/null \
        && firewall_close_port_firewall "$1" "$prot" && return 0
    systemctl is-active ufw >/dev/null \
        && firewall_close_port_ufw "$1" "$prot" && return 0
    [ -e /etc/init.d/iptables ] \
        && firewall_close_port_iptables "$1" "$prot"
}

# 重启防火墙
function firewall_reload(){
    if systemctl is-active firewalld >/dev/null 
    then
        echo '重启防火墙(reload firewall)--firewalld'
        sudo firewall-cmd --reload || exit_on_error
        return 
    fi
    if systemctl is-active ufw >/dev/null 
    then
        echo '重启防火墙(reload firewall)--ufw' 
        sudo ufw reload || exit_on_error
        return 
    fi

    if [ -e /etc/init.d/iptables ]
    then 
        echo '重启防火墙(reload firewall)--iptables'
        sudo service iptables save || exit_on_error
        sudo service iptables restart || exit_on_error
        return 
    fi
}

# 删除安装的服务(systemcl管理)
# $1 服务名
function systemctl_remove(){
    [ -z "$1" ] && return 0
    if ! sudo systemctl | grep -q -F $1.service 
    then
        echo $1 服务未安装 && return 0
    fi

    echo "删除 $1 服务"
    sudo systemctl stop $1 || exit_on_error
    sudo systemctl disable $1 || exit_on_error

    if [ -f /etc/systemd/system/$1.service ]
    then
        sudo rm -f /etc/systemd/system/$1.service || exit_on_error
    fi

    if [ -f /etc/init.d/$1 ]
    then
        sudo rm -f /etc/init.d/$1 || exit_on_error
    fi 
    sudo systemctl daemon-reload || exit_on_error    

}

# 删除指定的 mysql 数据库
# $1 mysql host
# $2 mysql port
# $3 mysql username
# $4 mysql password
# $5 mysql database name
# $6 application name 
function mysql_remove_database(){    
    ! which mysql >/dev/null 2>&1 && return 0
    [ -n "$1" ] || exit_on_error "无效的输入参数 \$1(host)"
    [ -n "$2" ] || exit_on_error "无效的输入参数 \$2(port)"
    [ -n "$3" ] || exit_on_error "无效的输入参数 \$3(username)"
    [ -n "$5" ] || exit_on_error "无效的输入参数 \$5(database)"
    export MYSQL_PWD=$4
    local conn_param="-h$1 -P$2 -u$3"
    echo "删除 mysql://$1:$2 上的$6数据库 ($5)"
       mysql $conn_param -e "drop database if exists $5" || exit_on_error
}

# 返回所有ip地址及对应的物理网卡
function nic_info(){
    for nic in $(route -n | grep  ^0.0.0.0 | awk '{print $8}') 
    do    
        local ip=$(ifconfig $nic | grep -E 'inet\s+'|awk '{print $2}'|sed -e 's/.*://g')
        echo $ip[$nic]
    done
}

# 检查 主机名是否为本机地址,是返回0,否则返回1
# $1 $host
function is_local_address(){
    [ -z "$1" ] && return 1
    [[ "$1" =~ (localhos|127.0.0.1) ]] && return 0
    local ips=($(nic_info))
    echo "$(nic_info)" | grep -q -F "$1" && return 0
    return 1
}
# 检查输入的参数是否为有效的ip地址,是返回0,否则返回1
# $1 ip address
function is_valid_ip(){
    echo "$1" | grep -P -e '^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$'
}

# 检查输入的参数是否为有效的主机名,域名或ip地址,是返回0,否则返回1
# $1 host
function is_valid_host(){
    echo "$1" | grep -P -e '^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})*$' >/dev/null    
}
# 检查输入的参数是否为有效的域名,是返回0,否则返回1
# $1 domain name
function is_valid_domain_name(){
    ! is_valid_ip "$1" && is_valid_host "$1"
}

# 如果存在备份文件(.facelog_bak)则从备份文件恢复指定文件,并删除备份
# $1 要备份的文件
function backup_facelog_bak(){
    [ -z "$1" ] && return 
    [ -f "$1" ] || exit_on_error "要备份的文件 $1 不存在"
    echo "备份(backup to $1.facelog_bak)"
    sudo cp -f "$1" "$1.facelog_bak" || exit_on_error
}

# 如果存在备份文件(.facelog_bak)则从备份文件恢复指定文件,并删除备份
# $1 要恢复的文件
function restore_facelog_bak(){
    [ -z "$1" ]  && return 
    if [ -f $1.facelog_bak ]
    then
        echo "从备份恢复(restore $1)"
        sudo cp -f $1.facelog_bak $1 || exit_on_error
        sudo rm -f $1.facelog_bak    || exit_on_error
    fi
}
# 版本号比较 >
function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; }
# 版本号比较 <=
function version_le() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" == "$1"; }
# 版本号比较 <
function version_lt() { test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" != "$1"; }
# 版本号比较 >=
function version_ge() { test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" == "$1"; }

# 检查脚本是否支持当前操作系统不支持则报错退出
function chedk_supported_os(){
    which systemctl >/dev/null 2>&1 || \
        exit_on_error "脚本不支持当前系统下的自动部署 $(uname -a)" 
    if which getenforce >/dev/null 2>&1
    then
        [ "$(getenforce)" != 'Enforcing' ]  || \
            exit_on_error "SELinux 开启,能力所限，目前不支持 SELinux 开启下的自动部署,请关闭SELinux再安装"
    fi
}

# 安装基础工具
# 该方法中引用的函数( is_installed_pkg,install_chinese_support,install_components)和变量 (BASE_TOOL_PKGS) 
# 需要平台依赖的 native 函数支持
function install_base_utils(){
    local pkg_list=
    # 先查检是否安装 coreutils,net-tools，如果未安装则安装
    is_installed_pkg coreutils || pkg_list="$pkg_list coreutils"
    is_installed_pkg net-tools || pkg_list="$pkg_list net-tools"
    for cmd_name in ${!BASE_TOOL_PKGS[@]}
    do
        which $cmd_name >/dev/null 2>&1 && continue
        pkg_list="$pkg_list ${BASE_TOOL_PKGS[$cmd_name]}"
    done

    [ -z "$pkg_list" ] || install_components $pkg_list
    install_chinese_support
}
# mysql server 版本号
# 失败则报错退出，成功则返回值保存在 mysqld_version 变量中
function get_mysqld_version(){
    local mysql_version="$(mysqld --version | grep -o -E "Ver\s+\S+" | awk '{print $2}' | grep -o -P '^\d+\.\d+\.\d+')" 
    [ -n "$mysql_version" ] || exit_on_error 'ERROR:无法获取 mysql server 版本号' 
    mysqld_version="$mysql_version" 
}

# 返回 java version,如果没有找到 java,返回空,EXIT COD返回1
# $1 java执行路径,为空则为 java
function get_java_version(){
    local exe=java
    [ -n "$1" ] && exe="$1"
    local v=$($exe -version 2>&1 | sed -e '1!d' -e 's/"//g')
    echo "$v" | grep -q 'version' && echo "$v" | awk '{print $3}'    
}

# 返回 javac version,如果没有找到 java,返回空,EXIT COD返回1
# $1 javac 执行路径,为空则为 javac
function get_javac_version(){
    local exe=javac
    [ -n "$1" ] && exe="$1"
    local v=$($exe -version 2>&1)
    echo "$v" | grep -q '^javac' && echo "$v" | awk '{print $2}'    
}
# 返回 node version,如果没有找到 node,返回空,EXIT COD返回1
# $1 node 执行路径,为空则为 node
function get_node_version(){
    local exe=node
    [ -n "$1" ] && exe="$1"
    which "$exe" >/dev/null 2>&1 || return 1    
    if [ "$(uname -o)" = 'Msys' ]    
    then 
        # msys下需要用bash执行否则会报错：'stdout is not a tty'
        local v=$(bash -c "$exe --version 2>/dev/null")
    else
        local v=$($exe --version 2>/dev/null)
    fi
    echo "$v" | grep -q -P '^v\d+' && echo "$v" | sed 's/^v//'        
}

# 返回 maven version,如果没有找到 maven,返回空,EXIT COD返回1
# $1 mvn 执行路径,为空则为 mvn
function get_maven_version(){
    local exe=mvn
    [ -n "$1" ] && exe="$1"
    which "$exe" >/dev/null 2>&1 || return 1
    local v=$("$exe" --version 2>/dev/null | sed '1!d')
    echo "$v" | grep -q -P 'Apache\s+Maven\s+\S+' && echo "$v" | awk '{print $3}'
}

# 检查 jdk 7 是否可用,如果java不是指定类型的 jdk7 则返回1,否则返回 0
# $1 java exe路径，为空则默认为java,路径无效返回1
function jdk7_available(){
    version_ge "$(get_javac_version "$1")" 1.7   
}

# 检查 jdk 8 是否可用,是则返回1,否则返回 0
# $1 java exe路径，为空则默认为java,路径无效返回1
function jdk8_available(){
    version_ge "$(get_javac_version "$1")" 1.8    
}

# 检查 node 是否可用,如果可用且版本号小于10.15则返回0,否则返回 1
# $1 node exe路径，为空则默认为 node ,路径无效返回 1
function node_available(){
    version_ge "$(get_node_version "$1")" 10.15
}

# 检查 maven 是否可用,如果可用且版本号小于3.5.0则返回0,否则返回 1
# $1 mvn exe路径，为空则默认为 mvn ,路径无效返回 1
function maven_available(){
    version_ge "$(get_maven_version "$1")" 3.5.0
}
# 检查 git 是否可用,是则返回1,否则返回 0
function git_available(){
    which git >/dev/null
}
function need_install_java8(){
    jdk8_available "$java_exe" &&  return 1
    return 0
}

function need_install_node(){
    node_available "$node_exe" &&  return 1
    return 0
}

# (unzip)解压指定的程序包到目标文件夹
# $1 安装包名不包括后缀.zip
# $2 目标文件夹名,不填为unzip默认文件夹
function decompress_package(){
    [ -d $PREFIX/$1 ] && [ "$force_extract" != 'true' ] && echo "文件夹 $PREFIX/$1 已存在,跳过" && return    
    if [ -d $PREFIX/$1 ]
    then
        echo "删除文件夹 $PREFIX/$1 ..." 
        rm $PREFIX/$1 -fr || exit 255
    fi
    ## 解压缩失败则退出安装脚本,解压前删除已经存在的文件夹
    echo "解压缩 $PKG_FOLDER/$1.zip 到 $PREFIX/$2" 
    unzip -q $PKG_FOLDER/$1.zip -d $PREFIX/$2 || exit 255
}

# 如果文件存在且checksum与$2指定的md5相等则返回 1,否则返回0
# $1 待检查的文件路径
# $2 md5校验码
# 参数错误报错退出
# force_download为true时总是返回0
function need_download(){
    [ "$force_download" = 'true' ] && return 0
    [ $# -ne 2 ] && echo "无效参数: $*" && exit 255
    [ -z "$2" ] && echo "没有提供MD5参数 \$2 " && exit 255
    [ ! -f "$1" ] && return 0
    local checksum=`md5sum $1 | awk '{ print $1 }'`
    if [ "$checksum" = "$2" ]; then
        return 1
    else
        return 0
    fi
}
# 下载指定的安装包
# 如果安装包不存在或文件不完整则下载
# $1 安装包名
# $2 安包名的MD5校验码
# $3 安装包下载url
function download_package(){
    need_download "$PKG_FOLDER/$1" $2 || { echo "$PKG_FOLDER/$1 存在,跳过"; return; }
    mkdir -p "$PKG_FOLDER" || exit 255 
    if [ "$force_download" = 'true' ] && [ -f "$PKG_FOLDER/$1" ]
    then 
        # 强制下载时先清除已有文件
        rm "$PKG_FOLDER/$1" && echo "删除原有文件 $PKG_FOLDER/$1" || exit 255
    fi
    if which wget >/dev/null 2>&1
    then
        # -c 断点续传
        # -O 保存为指定的文件
        wget --no-check-certificate $3 -c -O "$PKG_FOLDER/$1" || exit 255
    elif which curl >/dev/null 2>&1
    then
        # -L 自动跳转
        # -o 保存为指定的文件
        curl -L $3 -o "$PKG_FOLDER/$1" || exit 255
    else
        echo "没有找到合适的下载工具(wget OR curl)"
        exit 255
    fi
}

# 安装编译项目需要的依赖软件
function install_build_tools_if_needed(){
    local os_name="$(uname -o)"
    [ "$os_name" = 'GNU/Linux' ] || exit_on_error "不支持的平台 $os_name"
    local need_source=1
    install_components "$(need_install_build_tools)"
    if ! maven_available 
    then
        if ! maven_available "$(get_maven_version $PREFIX/$MVN/mvn)"
        then
            echo "检查/下载 maven 安装包...(download maven packages)"
            download_package $MVN.zip  $MVN_MD5  $MVN_URL
        echo "解压缩 maven 安装包...(install maven packages)"
            decompress_package $MVN
        fi
        echo "修改 $HOME/.bashrc"
        local m2_home="$(realpath "$PREFIX/$MVN")"
        echo "export PATH=$m2_home"
        set_value $HOME/.bashrc M2_HOME "$m2_home"
        sed -i 's!^\s*M2_HOME=!export M2_HOME=!' $HOME/.bashrc || exit_on_error
        echo 'export PATH=$M2_HOME/bin:$PATH'
        sed -i '/^export M2_HOME=/aexport PATH=$M2_HOME/bin:$PATH' $HOME/.bashrc || exit_on_error
        need_source=0
    fi
    if [ ! -d "$JAVA_HOME" ]
    then
        echo "修改 $HOME/.bashrc"
        local java_home="$(readlink -f $(which javac))"
        java_home="$(dirname $java_home)"
        java_home="$(dirname $java_home)"
        echo "JAVA_HOME=$java_home"
        set_value $HOME/.bashrc JAVA_HOME "$java_home"
        sed -i 's!^\s*JAVA_HOME=!export JAVA_HOME=!' $HOME/.bashrc || exit_on_error
        echo 'export PATH=$JAVA_HOME/bin:$PATH'
        sed -i '/^export JAVA_HOME=/aexport PATH=$JAVA_HOME/bin:$PATH' $HOME/.bashrc || exit_on_error
        need_source=0
    fi
    [ $need_source -eq 0 ] && echo '登录环境已经改变,请执行 source ~/.bashrc 更新当前登录环境后再重新执行' &&  exit 1

}
